package org.zoomdev.zoom.dao.impl;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zoomdev.zoom.common.utils.CachedClasses;
import org.zoomdev.zoom.common.utils.Classes;
import org.zoomdev.zoom.common.utils.NameCaseConverter;
import org.zoomdev.zoom.dao.Dao;
import org.zoomdev.zoom.dao.annotations.AutoGenerate;
import org.zoomdev.zoom.dao.annotations.ColumnIgnore;
import org.zoomdev.zoom.dao.annotations.PrimaryKey;
import org.zoomdev.zoom.dao.annotations.UniqueKey;
import org.zoomdev.zoom.dao.ddl.DatabaseBuilder;
import org.zoomdev.zoom.dao.driver.SqlDriver;
import org.zoomdev.zoom.dao.meta.ColumnMeta;
import org.zoomdev.zoom.dao.meta.DefaultValue;
import org.zoomdev.zoom.dao.meta.KeyType;
import org.zoomdev.zoom.dao.modules.CoreDaoModule;
import org.zoomdev.zoom.dao.transaction.Transactions;

import java.io.File;
import java.lang.reflect.Field;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public final class ZoomDatabaseBuilder implements DatabaseBuilder {


    public static final int DATETIME = 10000;
    private static final Logger log = LoggerFactory.getLogger(CoreDaoModule.NAME);
    private final Dao dao;
    private final List<TableBuildInfo> tables = new ArrayList<TableBuildInfo>();
    private final SqlDriver driver;
    private final List<BuildInfo> buildInfo = new ArrayList<BuildInfo>();
    private TableBuildInfo tableBuildInfo;
    private ColumnMeta columnMeta;

    public ZoomDatabaseBuilder(Dao dao) {
        this.dao = dao;
        this.driver = dao.getDriver();
    }

    private static abstract class BuildInfo {
        abstract void build(List<String> sqls);
    }    @Override
    public DatabaseBuilder dropIfExists(String table) {
        buildInfo.add(new DropTableIfExists(table));
        return this;
    }

    private class CombineUniqueKey extends BuildInfo {

        private final String table;
        private final String[] keys;

        public CombineUniqueKey(String table, String[] keys) {
            this.table = table;
            this.keys = keys;
        }

        @Override
        void build(List<String> sqls) {
            List<String> protectedKeys = Arrays.stream(keys).map(k -> driver.protectColumn(k)).collect(Collectors.toList());
            sqls.add("ALTER TABLE " + table + " ADD UNIQUE KEY UNI_" +
                    StringUtils.join(keys, "_") + "(" + StringUtils.join(protectedKeys, ",") + ")");
        }
    }

    private class DropTableIfExists extends BuildInfo {

        private final String table;

        public DropTableIfExists(String table) {
            this.table = table;
        }

        @Override
        void build(List<String> sqls) {

            sqls.add(driver.buildDropIfExists(table));


        }
    }    @Override
    public DatabaseBuilder createIfNotExists(String table) {
        createTable(table);
        tableBuildInfo.createWhenNotExists = true;

        return this;
    }

    private class CreateTable extends BuildInfo {

        private final TableBuildInfo table;


        public CreateTable(TableBuildInfo table) {
            this.table = table;
        }

        @Override
        void build(List<String> sqls) {
            driver.buildTable(table, sqls);
        }
    }

    @Override
    public DatabaseBuilder comment(String comment) {
        if (columnMeta != null) {
            columnMeta.setComment(comment);

        } else if (tableBuildInfo != null) {
            tableBuildInfo.comment = comment;
        }

        return this;
    }



    @Override
    public DatabaseBuilder createTable(String table) {
        tableBuildInfo = new TableBuildInfo();
        tableBuildInfo.name = table;
        tables.add(tableBuildInfo);
        buildInfo.add(new CreateTable(tableBuildInfo));

        columnMeta = null;
        return this;
    }



    @Override
    public DatabaseBuilder add(String column) {

        columnMeta = new ColumnMeta();
        tableBuildInfo.columns.add(columnMeta);
        columnMeta.setNullable(true);
        columnMeta.setName(column);

        return this;
    }

    @Override
    public DatabaseBuilder modify(String table, String column) {
        return this;
    }

    @Override
    public DatabaseBuilder string(int len) {
        columnMeta.setType(Types.VARCHAR);
        columnMeta.setMaxLen(len);
        return this;
    }

    @Override
    public DatabaseBuilder charString(int len) {
        columnMeta.setType(Types.CHAR);
        columnMeta.setMaxLen(len);
        return this;
    }

    @Override
    public DatabaseBuilder nstring(int len) {
        columnMeta.setType(Types.NVARCHAR);
        columnMeta.setMaxLen(len);
        return this;
    }

    @Override
    public DatabaseBuilder smallInt() {
        columnMeta.setType(Types.SMALLINT);
        columnMeta.setUnsigned(false);
        return this;
    }

    @Override
    public DatabaseBuilder smallIntUnsigned() {
        columnMeta.setType(Types.SMALLINT);
        columnMeta.setUnsigned(true);
        return this;
    }

    @Override
    public DatabaseBuilder text() {
        columnMeta.setType(Types.CLOB);
        return this;
    }

    @Override
    public DatabaseBuilder bool() {
        columnMeta.setType(Types.BOOLEAN);
        return this;
    }

    @Override
    public DatabaseBuilder decimal(int n, int m) {
        columnMeta.setType(Types.DECIMAL);
        columnMeta.setDecimal(n, m);
        return this;
    }

    @Override
    public DatabaseBuilder timestamp() {
        columnMeta.setType(Types.TIMESTAMP);
        return this;
    }


    @Override
    public DatabaseBuilder datetime() {
        columnMeta.setType(DATETIME);
        return this;
    }

    @Override
    public DatabaseBuilder date() {
        columnMeta.setType(Types.DATE);
        return this;
    }

    @Override
    public DatabaseBuilder integer() {
        columnMeta.setType(Types.INTEGER);
        return this;
    }

    @Override
    public DatabaseBuilder integerUnsigned() {
        columnMeta.setType(Types.INTEGER);
        columnMeta.setUnsigned(true);
        return this;
    }

    @Override
    public DatabaseBuilder bigInt() {
        columnMeta.setType(Types.BIGINT);
        return this;
    }

    @Override
    public DatabaseBuilder bigIntUnsigned() {
        columnMeta.setType(Types.BIGINT);
        columnMeta.setUnsigned(true);
        return this;
    }

    @Override
    public DatabaseBuilder tinyInt() {
        columnMeta.setType(Types.TINYINT);
        return this;
    }

    @Override
    public DatabaseBuilder tinyIntUnsigned() {
        columnMeta.setType(Types.TINYINT);
        columnMeta.setUnsigned(true);
        return this;
    }

    @Override
    public DatabaseBuilder clob() {
        columnMeta.setType(Types.CLOB);
        return this;
    }

    @Override
    public DatabaseBuilder number() {
        columnMeta.setType(Types.NUMERIC);
        return this;
    }


    @Override
    public DatabaseBuilder notNull() {
        columnMeta.setNullable(false);
        return this;
    }

    @Override
    public DatabaseBuilder keyPrimary() {
        columnMeta.setKeyType(KeyType.PRIMARY);
        return this;
    }

    @Override
    public DatabaseBuilder autoIncement() {
        columnMeta.setAuto(true);
        return this;
    }

    @Override
    public DatabaseBuilder keyUnique() {
        columnMeta.setKeyType(KeyType.UNIQUE);
        return this;
    }

    @Override
    public DatabaseBuilder keyIndex() {
        columnMeta.setKeyType(KeyType.INDEX);
        return this;
    }

    @Override
    public DatabaseBuilder engine(String engine) {
        this.tableBuildInfo.engine = engine;
        return this;
    }

    @Override
    public String buildSql() {
        List<String> list = new ArrayList<String>();


        for (BuildInfo buildInfo : buildInfo) {
            buildInfo.build(list);
        }


        StringBuilder sb = new StringBuilder();

        for (String sql : list) {
            sb.append(sql);
            if (!sql.endsWith(";")) {
                sb.append(";");
            }
            sb.append("\n");
        }

        return sb.toString();

    }

    @Override
    public void build() {
        final List<String> list = new ArrayList<String>();
        Transactions.executeTrans(() -> {
            for (BuildInfo buildInfo : buildInfo) {
                buildInfo.build(list);
                for (String str : list) {
                    dao.ar().execute(str);
                }

                list.clear();
            }
        });


    }

    @Override
    public void build(Class<?> type, boolean dropIfExists) {
        assert (type != null);
        //build this table
        String table = TableNameUtils.getTableName(type);
        if (dropIfExists) {
            this.dropIfExists(table);
            createTable(table);
        } else {
            createIfNotExists(table);
        }


        //fields

        Field[] fields = CachedClasses.getFields(type);
        for (Field field : fields) {

            if (field.isAnnotationPresent(ColumnIgnore.class)) {
                continue;
            }
            String columnName = NameCaseConverter.toSnakeCase(field.getName());
            add(columnName);
            Class<?> fieldType = field.getType();
            if (Classes.isString(fieldType)) {
                string(200);
            } else if (Classes.isBoolean(fieldType)) {
                bool();
            } else if (Classes.isInteger(fieldType)) {
                integer();
            } else if (Classes.isNumber(fieldType)) {
                number();
            } else if (Classes.isDateTime(fieldType)) {
                date();
            } else if (Classes.isEnum(fieldType)) {
                string(30);
            } else if (Map.class.isAssignableFrom(fieldType)) {
                clob();
            } else if (Iterable.class.isAssignableFrom(fieldType)) {
                clob();
            } else if (byte[].class.isAssignableFrom(fieldType)) {
                blob();
            } else if (File.class.isAssignableFrom(fieldType)) {
                blob();
            } else {
                clob();
            }

            if (field.isAnnotationPresent(PrimaryKey.class)) {
                keyPrimary();
            }

            if (field.isAnnotationPresent(AutoGenerate.class)) {
                keyPrimary();
                autoIncement();
            }

            if (field.isAnnotationPresent(UniqueKey.class)) {
                keyUnique();
            }

        }


        build();
    }

    @Override
    public DatabaseBuilder defaultValue(Object value) {
        columnMeta.setDefaultValue(value);
        return this;
    }

    @Override
    public DatabaseBuilder blob() {
        columnMeta.setType(Types.BLOB);
        return this;
    }

    @Override
    public DatabaseBuilder mediumBlob() {
        columnMeta.setType(Types.BLOB);
        columnMeta.setSubType(ColumnMeta.MEDIUM);
        return this;
    }


    @Override
    public DatabaseBuilder defaultFunction(String value) {
        columnMeta.setDefaultValue(DefaultValue.function(value));
        return this;
    }

    @Override
    public DatabaseBuilder createCombineUniqueKey(String... names) {
        buildInfo.add(new CombineUniqueKey(tableBuildInfo.getName(), names));
        return this;
    }


}
